package chips440.server;

import chips440.main.Cell;
import chips440.main.Packet;
import chips440.main.Types;

import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.InetAddress;
import java.util.HashMap;

/**
 * This class defines a stand-alone server for networked chips440 games. This
 * runs entirely in command line mode, and dumps occasional data when clients
 * connect and such.
 *
 * @author Ian Voysey
 * @version 0.1
 */
public class Server{
    private int max_con = 1;
    private Engine the_game;
    private HashMap <Integer, Thread> client_list;

    // the size of the window of the board any client will need
    private static final int WIN_SIZE = 9;
    
    /**
     * The main method for launching a chips440 server.
     *
     * @param args the command line arguments; this should only be the port
     * number to run on
     */
    public static void main (String args[]){
	if(args.length != 1){
	    System.err.println("USAGE: java Server <port number>");
	    System.exit(1);
	}
	try{
	    Server myServer = new Server(Integer.parseInt(args[0]));
	} catch (NumberFormatException e){
	    System.err.println("FAIL: the argument must be an integer");
	    System.exit(1);
	}
    }

    public Server(int port_num){
	try{
	    System.err.printf("INFO: this server is running on %s port %d\n",
			      InetAddress.getLocalHost(), port_num);
	} catch (Exception e) {
	    System.err.printf("FAIL: can't get server IP; this is bad\n");
	    System.exit(1);
	}
	the_game = new Engine();
	client_list = new HashMap<Integer, Thread>();

	try{
	    ServerSocket server = new ServerSocket(port_num);
	    while(true){
		Socket clientSocket = server.accept();

		if(the_game.numPlayers() < max_con){
		    Thread t = new Thread(new ClientConnection(clientSocket));
		    client_list.put(new Integer(clientSocket.hashCode()), t);
		    t.start();
		    System.err.printf("INFO: new client connected at %s\n",
				      clientSocket.getInetAddress());
		}
		Thread.sleep(250);
		if(the_game.numPlayers() == max_con){
		    the_game.start();
		}
	    }
	} catch (Exception e){
	    System.out.println(e);
	}
    }

    /**
     * This class defines a thread to be assigned to each incomming
     * connection, allowing it to interact with the engine appropriately in
     * turn.
     *
     * @author Ian Voysey
     * @version 0.1
     */
    private class ClientConnection implements Runnable{
	private Socket socket;
	private int client_id;

	public ClientConnection(Socket socket){
	    // this is as pseudorandom as our player IDs need to be
	    this.client_id = socket.hashCode();
	    this.socket = socket;
	}

	/**
	 * This is called whenever the thread is woken up. It reads the
	 * desired action off the packet and then performs it, possibly
	 * letting the client know
	 */
	public void run(){
	    while(true){
		Packet from_client = get_packet(socket);
		if(from_client == null){
		    //   client_list.get(new Integer(client_id)).interrupt();
		    client_list.remove(new Integer(client_id));
		    the_game.kill();
		    the_game = new Engine();
		    return;
		}

		System.err.printf("INFO: %s\n", from_client.toString());
		switch (from_client.changes){
		case QUIT:
		    client_list.get(new Integer(client_id)).interrupt();
		    client_list.remove(new Integer(client_id));
		    the_game.kill();
		    the_game = new Engine();
		    break;

		case EMPTY:
		    break;

		case ID:
		    from_client.positions = the_game.getPlayerPosition(client_id);
		    the_game.add_player(client_id);
		    max_con = from_client.numPlay;
		    from_client.identity = client_id;
		    send_packet(socket, from_client);
		    break;

		case MOVE:
		    the_game.move(from_client.direct, client_id);
		    break;
		    
		case TILE: 
		    from_client.tile = the_game.getImage
			(from_client.tileX, from_client.tileY, WIN_SIZE);
		    send_packet(socket, from_client);
		    break;

		case TRADE:
		    the_game.giveItem(from_client.offering, client_id);
		    break;
		    
		case ALL:
		    from_client.positions = the_game.getPlayerPosition(client_id);
		    from_client.items = the_game.getItems(client_id);
		    from_client.state = the_game.getGameState();
		    from_client.time = the_game.getTimeLeft();
		    from_client.chips = the_game.getChipsLeft();
		    send_packet(socket, from_client);
		    break;
		}
	    }
	}

	/**
	 * Sends a packet with a particular request to the server
	 */
	private void send_packet(Socket socket, Packet packet){
	    try{
		packet.changes = Types.ChangeSet.EMPTY;

		ObjectOutputStream out =
		    new ObjectOutputStream(socket.getOutputStream());
		out.writeObject(packet);
		out.flush();
		//out.close();
		//System.err.println("INFO: send called");
	    }catch(IOException io){
		System.err.printf("WARN: mangled packet from client %d\n",
				  client_id);
		the_game.del_player(client_id);
		client_list.get(client_id).interrupt();
	    }
	}

	/**
	 * Retrieves a response from the server
	 */
	private Packet get_packet(Socket socket){
	    Packet packet = null;
	    try{
		ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
		packet = (Packet)in.readObject();
		//System.err.println("INFO: get called");
		//in.close();
	    }catch(Exception io){
		System.err.printf("WARN: mangled packet from client %d\n",
				  client_id);
		io.printStackTrace();
		return null;
	    }
	    return packet;
	}
    }
}